/* 
 * Copyright (c) 2012, Fromentin Xavier, Schnell Michaël, Dervin Cyrielle, Brabant Quentin
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *      * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *      * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *      * The names of its contributors may not be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Fromentin Xavier, Schnell Michaël, Dervin Cyrielle OR Brabant Quentin 
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package kameleon.gui.model;

import java.io.File;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;

import javax.swing.SwingWorker;

import kameleon.document.Document;
import kameleon.exception.InvalidPlugInException;
import kameleon.exception.KameleonException;
import kameleon.gui.exception.InvalidGenerationException;
import kameleon.gui.model.GenerationMessage.GenerationState;
import kameleon.gui.model.GenerationMessage.State;
import kameleon.plugin.Analyzer;
import kameleon.plugin.Generator;
import kameleon.plugin.PlugInInfo;
import kameleon.util.IOPlugIn;

/**
 * Model responsible for the generation process.
 * 
 * <p>//TODO ADD A LOT MORE DETAILS !
 * 
 * @author		Brabant Quentin, Dervin Cyrielle, Fromentin Xavier, Schnell Michaël
 * @version		1.0
 */
public class GenerationModel extends FileModel {

	/**
	 * Flag indicating that a generator has just been added
	 * or removed.
	 */
	protected boolean generatorAddedOrRemoved ;

	/**
	 * Flag indicating that the generation process has just 
	 * finished.
	 */
	protected boolean generationFinished ;

	/**
	 * Builds an instance with the given options.
	 * 
	 * <p>Reads the history and the the configuration files
	 * to find the installed plug-ins.
	 * 
	 * @param 	debugMode
	 * 			flag indicating whether the debug mode 
	 * 			should be activated ({@code true} means
	 * 			activated)
	 */
	public GenerationModel(boolean debugMode) {
		super(debugMode) ;
		this.generatorAddedOrRemoved = false ;
		this.selectedGenerators = new ArrayList<String>() ;
		try {
			this.initializePlugIns() ;
		} catch(KameleonException ke) {
			this.displayDebugInformation(ke) ;
		}// try
	}// GenerationModel(boolean)

	/**
	 * Builds an instance with default options.
	 */
	public GenerationModel() {
		this(DEFAULT_DEBUG_MODE) ;
	}// GenerationModel()

	/**
	 * Generates the absolute file paths for the files generated by 
	 * the selected generators.
	 * 
	 * <p>If only generator is selected, the model will only check is
	 * the given file extension is valid. If not, a valid (default) one
	 * is appended to the file name.
	 * 
	 * <p>If multiple generators are selected, the software will
	 * append {@code "_<format name>"} to each file name before the 
	 * valid extension. Same as before, if the given extension is not
	 * valid, a default one is added.
	 * 
	 * //TODO Add an example
	 * 
	 * @return	Array containing the generated names, the order of the 
	 * 			names is the same as the order of the generators
	 *///TODO Review null pointer
	protected String[] generateFileNames() {
		if (this.selectedGenerators.size() == 1) {
			return new String[]{generateName(
					this.outputFile.getAbsolutePath(), 
					this.getGenerator(this.selectedGenerators.get(0)))} ;
		}// if
		String[] names = new String[this.selectedGenerators.size()] ;
		Iterator<String> iter = this.selectedGenerators.iterator() ;
		String baseName = this.outputFile.getAbsolutePath() ;
		for(int n = 0; iter.hasNext(); ++n) {
			names[n] = generateExtendedName(baseName, 
					this.getGenerator(iter.next())) ;
		}// for
		return names ;
	}// generateFileNames()

	/**
	 * Generates the file name for a given generator using the provided
	 * name. Checks if the given extension is valid for the given
	 * generator and if is not, appends a valid one to the file name.
	 * If the given generator has no valid extension, the file name
	 * is left unchanged.
	 * 
	 * @param 	baseName
	 * 			absolute path of the wanted file
	 * 
	 * @param 	generatorInfo
	 * 			generator of the file whose name is created
	 * 
	 * @return	Absolute path for the file
	 *///TODO Review null pointers
	protected static String generateName(String baseName, PlugInInfo generatorInfo) {
		String ext = getExtension(baseName) ;
		StringBuilder name = new StringBuilder(baseName) ;
		if (!isValidExtension(ext, generatorInfo)) {
			String[] validExtensions = generatorInfo.getExtensions() ;
			if (validExtensions.length > 0) {
				name.append('.') ;
				name.append(validExtensions[0]) ;
			}// if
		}// if
		return name.toString() ;
	}// generateName(String, PlugInInfo)

	/**
	 * Generates the file name for a given generator using the provided
	 * name. Checks if the given extension is valid for the given
	 * generator and if is not, appends a valid one to the file name.
	 * If the given generator has no valid extension, the file name
	 * is left unchanged. Also adds {@code "_<format name>"} before
	 * the (valid) extension. This is done in order to prevent multiple
	 * generators from generate the same file.
	 * 
	 * @param 	baseName
	 * 			absolute path of the wanted file
	 * 
	 * @param 	generatorInfo
	 * 			generator of the file whose name is created
	 * 
	 * @return	Absolute path for the file
	 */
	protected static String generateExtendedName(String baseName, PlugInInfo generatorInfo) {
		String ext = getExtension(baseName) ;
		StringBuilder name = new StringBuilder(baseName) ;
		if (!isValidExtension(ext, generatorInfo)) {
			name.append('_') ;
			name.append(generatorInfo.getFormatName()) ;
			String[] validExtensions = generatorInfo.getExtensions() ;
			if (validExtensions.length > 0) {
				name.append('.') ;
				name.append(validExtensions[0]) ;
			}// if
		} else {
			int posDot = baseName.lastIndexOf('.') ;
			name.insert(posDot, generatorInfo.getFormatName()) ;
			name.insert(posDot, '_') ;
		}// if
		return name.toString() ;
	}// generateExtendedName(String, PlugInInfo)

	/**
	 * Indicates if the given extension is commonly used by the given
	 * plug-in (analyzer or generator).
	 * 
	 * @param 	extension
	 * 			extension which should be tested
	 * 
	 * @param 	generatorInfo
	 * 			plug-in for which the extension should be tested
	 * 
	 * @return	{@code true} if the plug-in commonly uses the given
	 * 			extension, {@code false} otherwise
	 * 
	 * @see		PlugInInfo
	 */
	protected static boolean isValidExtension(String extension, PlugInInfo generatorInfo) {
		boolean valid = (generatorInfo.getExtensions().length == 0) ;
		for(String ext : generatorInfo.getExtensions()) {
			valid = ext.equals(extension) ;
			if (valid) break ;
		}// if
		return valid ;
	}// isValidExtension(String, PlugInInfo)

	/**
	 * Returns an instance of the "main" class of the given plug-in.
	 * 
	 * @param	info
	 * 			information about he plug-in whose instance is requested
	 * 
	 * @param 	parentFolder
	 * 			absolute path the of the folder which contains the
	 * 			java archive of the plug-in
	 * 
	 * @param 	targetClass
	 * 			either {@code Analyzer.class} of {@code Generator.class}
	 * 
	 * @return	Instance of the main class of the given plug-in
	 * 
	 * @throws 	InvalidPlugInException
	 * 			If the main class could not be instantiated
	 * 
	 * @see		PlugInInfo
	 * @see		Analyzer
	 * @see		Generator
	 *///TODO Add exception for illegal argument
	private static <T> T getInstance(PlugInInfo info, String parentFolder, Class<T> targetClass) 
			throws InvalidPlugInException {
		try {
			ClassLoader loader = IOPlugIn.loadPlugIn(info, parentFolder) ;
			loader.loadClass(info.getPlugInClass()) ;
			Class<?> plugInClass = Class.forName(info.getPlugInClass(), true, loader) ;
			Class<? extends T> plClass = plugInClass.asSubclass(targetClass) ;
			Constructor<? extends T> constructor = plClass.getConstructor() ;
			return constructor.newInstance() ;
		} catch (Exception e) {
			/* === Possible exceptions ===
			 *  - ClassNotFoundException
			 *  - IllegalAccessException
			 *  - IllegalArgumentException
			 *  - InstantiationException
			 *  - InvocationTargetException
			 *  - NoSuchMethodException
			 *  - SecurityException
			 * === ------------------- === */
			throw new InvalidPlugInException(info, e) ;
		}// try
	}// getInstance(PlugInInfo, String, Class<T>)

	/**
	 * Launches the analyzing process with the given analyzer.
	 * 
	 * @param 	analyzer
	 * 			plug-in used to analyzed the currently selected file
	 * 
	 * @return	instance of {@code Document} resulting from the 
	 * 			analyzing process
	 * 
	 * @throws	InvalidGenerationException
	 * 			if the generation is impossible
	 * 
	 * @throws 	KameleonException
	 * 			if an error occurred while analyzing the selected file
	 */
	protected Document launchAnalyzer(PlugInInfo analyzer) throws KameleonException {
		if (!this.generationIsPossible()) {
			throw new InvalidGenerationException() ;
		}// if
		return GenerationModel.launchAnalyzer(analyzer, 
				this.getSelectedFileInfo().getPath()) ;
	}// launchAnalyzer(PlugInInfo)

	/**
	 * Launches the analyzing process with the given analyzer on the
	 * given file.
	 * 
	 * @param 	analyzer
	 * 			plug-in used to analyzed the given file
	 * 
	 * @param	path
	 * 			absolute path of the analyzed file
	 * 
	 * @return	instance of {@code Document} resulting from the 
	 * 
	 * @throws 	KameleonException
	 * 			if an error occurred while analyzing the given file
	 */
	public static Document launchAnalyzer(PlugInInfo analyzer, String path) 
			throws KameleonException {
		Analyzer plugIn = getInstance(analyzer, ANALYSER_FOLDER, Analyzer.class) ;
		return plugIn.analyze(path) ;
	}// launchAnalyzer(PlugInInfi, String)

	/**
	 * Launches the generation of the given file with the given 
	 * generator.
	 * 
	 * @param 	generator
	 * 			plug-in used to generate the given file
	 * 
	 * @param	document
	 * 			source for the content of the generated file
	 * 
	 * @param	targetFile
	 * 			absolute path of the generated file
	 * 
	 * @throws 	KameleonException
	 * 			if an error occurred while generating the file
	 */
	public static void launchGenerator(PlugInInfo generator, Document document, String targetFile) 
			throws KameleonException {
		Generator plugIn = getInstance(generator, GENERATOR_FOLDER, Generator.class) ;
		plugIn.generate(document, targetFile) ;
	}// launchGeneration(PlugInInfo, Document, String)

	/**
	 * Indicates if a generator has just been added or removed.
	 * 
	 * @return	{@code true} if a generator has just been added
	 * 			or removed, {@code false} otherwise
	 */
	public boolean generatorAddedOrRemoved() {
		return this.generatorAddedOrRemoved ;
	}// generatorAddedOrRemoved()
	
	/**
	 * Adds a new file. If the file is a plug-in installation file,
	 * the model will attempt to install the plug-in. Otherwise
	 * the model will add the file to history and select it.
	 * 
	 * @param	newFile
	 * 			added file
	 */
	@Override
	public void addFile(File newFile) {
		// Test if the added file is a plug-in
		String extension = getExtension(newFile) ;
		if (PLUGIN_EXTENSION.equals(extension)) {
			// Install the plug-in
			this.addPlugIn(newFile) ;
			return ;
		}// if
		super.addFile(newFile) ;
	}// addFile(File)

	/**
	 * {@inheritDoc}
	 * 
	 * @return	{@code PlugInInfo} about the newly added plug-in
	 * 			or {@code null} if the plug-in could not be added
	 */
	@Override
	public PlugInInfo addPlugIn(File plugin) {
		PlugInMessage pmsg = new PlugInMessage(true, null) ;
		try {
			pmsg.setMessage(ADD_PLUG_IN_START_MESSAGE) ;
			this.addMessage(pmsg) ;

			PlugInInfo info = super.addPlugIn(plugin) ;

			pmsg.setState(PlugInMessage.State.SUCCESS) ;
			pmsg.setMessage(
					info.isAnalyzer() ? ADD_ANALYZER_SUCESS_MESSAGE 
							          : ADD_GENERATOR_SUCESS_MESSAGE,
					info.getFormatName()) ;
			
			this.generatorAddedOrRemoved = !info.isAnalyzer() ;
			this.updateMessage(pmsg) ;
			this.generatorAddedOrRemoved = false ;

			return info ;
		} catch (KameleonException ke) {
			this.displayDebugInformation(ke) ;
			pmsg.setState(PlugInMessage.State.ERROR) ;
			pmsg.setMessage(ADD_PLUG_IN_ERROR_MESSAGE) ;
			this.updateMessage(pmsg) ;
		}// try

		return null ;
	}// adPlugIn(File)

	/**
	 * {@inheritDoc}
	 * 
	 * @return	{@code PlugInInfo} about the newly added plug-in
	 * 			or {@code null} if the plug-in could not be added
	 */
	@Override
	public PlugInInfo addPlugIn(String pluginPath) {
		return this.addPlugIn(new File(pluginPath)) ;
	}// addPlugIn(String)

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void removeAnalyzer(String analyzerId) {
		this.removePlugIn(this.getAnalyzer(analyzerId)) ;
	}// removeAnalyzer(String)

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void removeGenerator(String generatorId) {
                this.removeSelectedFormat(generatorId);
		this.removePlugIn(this.getGenerator(generatorId)) ;
	}// removeGenerator(String)

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void removePlugIn(PlugInInfo info) {
		PlugInMessage pmsg = new PlugInMessage(false, null) ;
		try {
			pmsg.setMessage( 
					info.isAnalyzer() ? REMOVE_ANALYZER_START_MESSAGE 
								      : REMOVE_GENERATOR_START_MESSAGE,
					info.getFormatName()) ;
			this.addMessage(pmsg) ;

			super.removePlugIn(info) ;

			pmsg.setState(PlugInMessage.State.SUCCESS) ;
			pmsg.setMessage( 
					info.isAnalyzer() ? REMOVE_ANALYZER_SUCESS_MESSAGE 
							          : REMOVE_GENERATOR_SUCESS_MESSAGE,
					info.getFormatName()) ;

			this.generatorAddedOrRemoved = true ;
			this.updateMessage(pmsg) ;
			this.generatorAddedOrRemoved = false ;
		} catch (KameleonException ke) {
			this.displayDebugInformation(ke) ;
			pmsg.setState(PlugInMessage.State.ERROR) ;
			pmsg.setMessage( 
				info.isAnalyzer() ? REMOVE_ANALYZER_ERROR_MESSAGE
							      : REMOVE_GENERATOR_ERROR_MESSAGE,
				info.getFormatName()) ;
			this.updateMessage(pmsg) ;
		}// try
	}// removePlugIn(PlugInInfo)

	/**
	 * Launches generation in a new thread using the options of the 
	 * given message.
	 * 
	 * @param 	message
	 * 			information about the generation
	 */
	public void fastGeneration(GenerationMessage message) {
		message.initializeStates() ;
		this.updateMessage(message) ;
		new GenerationThread(this, message).execute() ;
	}// fastGeneration(GenerationMessage)

	/**
	 * Launches the generation for the selected file with the
	 * selected generators. If the generation is impossible,
	 * this function does nothing.
	 */
	public void launchGeneration() {
		if (this.generationIsPossible()) {
			// Initialize message
			GenerationMessage message = new GenerationMessage(
					this.getSelectedFileInfo()) ;
			message.setLastGeneration(new Date()) ;// now
                        System.out.println(this.selectedGenerators);
			message.setTargetFormatId(this.selectedGenerators.toArray(
					new String[this.selectedGenerators.size()])) ;
			message.setTargetPaths(this.generateFileNames()) ;
			this.addMessage(message) ;
			
			// Do the actual generation
			this.fastGeneration(message) ;

			// Update hisotry
			FileInfo current = this.getSelectedFileInfo() ;
			current.setLastGeneration(message.getLastGeneration()) ;
			current.setTargetFormatId(message.getTargetFormatId()) ;
			current.setTargetPaths(message.getTargetPath()) ;
			this.writeHistory() ;
		}// if
	}// launchGeneration()

	/**
	 * Indicates whether the generation process has just finished.
	 * 
	 * @return	{@code true} if the generation process has just
	 * 			finished, {@code false} otherwise
	 */
	public boolean generationFinished() {
		return this.generationFinished ;
	}// generationFinished()

	/**
	 * Notifies the observers that the current generation process
	 * has just finished.
	 * 
	 * @param 	message
	 * 			message associated with the finished generation
	 */
	public void notifyGenerationFinished(Message message) {
		this.generationFinished = true ;		
		this.updateMessage(message) ;		
		this.generationFinished = false ;	
	}// notifyGenerationFinished(Message)

	/**
	 * Subclass of {@link SwingWorker} used to host the generation
	 * process.
	 * 
	 * @author	Schnell Michaël
	 * @version	1.0
	 */
	private class GenerationThread extends SwingWorker<Void, Void> {

		/**
		 * Model of the graphical interface.
		 */
		private GenerationModel model ;
		
		/**
		 * Message associated with this generation process.
		 */
		private GenerationMessage message ;

		/**
		 * Builds an instance with the given values.
		 * 
		 * @param 	model
		 * 			model of the graphical interface
		 * 
		 * @param 	message
		 * 			message containing the information about the
		 * 			generation process
		 */
		public GenerationThread(GenerationModel model, GenerationMessage message) {
			super();
			this.model = model ;
			this.message = message ;
		}// GenerationThread(GenerationModel, GenerationMessage)

		/**
		 * Executes the generation of the files requested in the
		 * given message as a background task.
		 */
		@Override
		protected Void doInBackground() {
			int rowIndex = 0 ;			
			String[] outputFiles = this.message.getTargetPath() ;
			String[] genIds = this.message.getTargetFormatId() ;
			boolean fileNotGenerated = false ;
			try {
				PlugInInfo analyzer = this.model.getAnalyzer(
						this.message.getIdFormat()) ;
				Document document = GenerationModel.launchAnalyzer(
						analyzer, this.message.getPath()) ;

				this.message.setState(GenerationState.GENERATING) ;
				this.model.updateMessage(this.message) ;
				
				for(rowIndex=0; rowIndex<genIds.length; ++rowIndex) {
					this.message.setState(rowIndex, State.CONVERTING) ;
					this.model.updateMessage(this.message) ;

					String genId = genIds[rowIndex] ;
					PlugInInfo generator = this.model.getGenerator(genId) ;
					try {
						GenerationModel.launchGenerator(
								generator, document, outputFiles[rowIndex]) ;

						this.message.setState(rowIndex, State.CONVERTED) ;
					} catch (KameleonException ke) {
						this.message.setState(rowIndex, State.ERROR) ;
						fileNotGenerated = true ;
					}// try
					this.model.updateMessage(this.message) ;
				}// for
				
				this.message.setLastGeneration(new Date()) ;
				if (fileNotGenerated) {
					this.message.setState(GenerationState.PARTIAL_SUCESS)  ;
				} else {
					this.message.setState(GenerationState.COMPLETE_SUCESS)  ;
				}// if
				this.model.updateMessage(this.message) ;
			} catch (KameleonException ke) {
				while (rowIndex < genIds.length) {
					this.message.setState(rowIndex, State.ERROR) ;
					rowIndex++ ;
				}// while
				this.message.setState(GenerationState.ERROR) ;
				this.model.updateMessage(this.message) ;
				this.model.displayDebugInformation(ke) ;
			}// try
			this.model.notifyGenerationFinished(this.message) ;
			
			return null ;
		}// doInBackground()
		
	}// class GenerationProcess

}// class Model